package de.playmee.handler;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;

import javax.xml.parsers.ParserConfigurationException;

import org.apache.http.client.ClientProtocolException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import de.playmee.Channel;
import de.playmee.ChannelTrackRating;
import de.playmee.Playlist;
import de.playmee.Track;
import de.playmee.Weather;
import de.playmee.XMLParser;
import de.playmee.enums.Mood;
import de.playmee.enums.Season;
import de.playmee.enums.TimeOfDay;

public class PlaylistHandler extends DatabaseHandler {

	private final String TAG = "PlaylistHandler";
	
	public final static String KEY_ID = "_id";
	public final static String KEY_CHANNEL = "channel";
	public final static String KEY_TRACKS = "tracks";
	
	private final static String DELIMITER = ",";
	
	public final static String DATABASE_TABLE = "playlist";
	
	public final static String DATABASE_CREATE = "create table " + DATABASE_TABLE
			+ " (" + KEY_ID + " integer primary key autoincrement, "
			+ KEY_CHANNEL + " bigint not null, "
			+ KEY_TRACKS + " text not null);";
	
	private MusicHandler mHandler;
	private Playlist currentPlaylist;
	private Track currentTrack;
	private ChannelTrackRatingHandler ctrHandler;
	
	public PlaylistHandler(DatabaseHelper dbHelper, MusicHandler mHandler,
			ChannelTrackRatingHandler ctrHandler, Weather currentWeatherConditions) {
		super(dbHelper);
		// now available: dbHelper, db, DELIMITER
		this.mHandler = mHandler;
		this.ctrHandler = ctrHandler;
	}
	
	/**
	 * sets the current playlist
	 * 
	 * @param currentPlaylist
	 */
	public void setCurrentPlaylist(Playlist currentPlaylist) {
		this.currentPlaylist = currentPlaylist;
		this.currentTrack = null;
	}
	
	/**
	 * @return currentPlaylist
	 */
	public Playlist getCurrentPlaylist() {
		return currentPlaylist;
	}
	
	/**
	 * inserts the current playlist into the database
	 * 
	 * @return {@link #insertPlaylist(Playlist)}
	 */
	public boolean insertPlaylist() {
		return insertPlaylist(currentPlaylist);
	}
	
	/**
	 * inserts a playlist into the database
	 * 
	 * @param playlist
	 * @return true if playlist was inserted successfully, false otherwise
	 */
	public boolean insertPlaylist(Playlist playlist) {
		Log.i(TAG, "Inserting playlist...");
		ContentValues initialValues = getContentValues(playlist);
		synchronized(db) {
			long id = db.insert(DATABASE_TABLE, null, initialValues);
			if (id != -1) {
				playlist.setId(id);
				return true;
			}
		}
		return false;
	}
	
	/**
	 * deletes the current playlist
	 * 
	 * @return {@link #deletePlaylist(long)}
	 */
	public boolean deletePlaylist() {
		return deletePlaylist(currentPlaylist.getChannelId());
	}
	
	/**
	 * deletes a particular playlist
	 * 
	 * @param _channelId
	 * @return {@link SQLiteDatabase#delete(String, String, String[])}
	 */
	public boolean deletePlaylist(long _channelId) {
		Log.i(TAG, "Deleting playlist " + _channelId + "...");
		synchronized(db) {
			return db.delete(DATABASE_TABLE, KEY_CHANNEL + "=" + _channelId, null) > 0;
		}
	}
	
	/**
	 * reload the current playlist
	 * 
	 * @return {@link #loadPlaylist(long)}
	 */
	public Playlist loadPlaylist() {
		return loadPlaylist(currentPlaylist.getChannelId());
	}
	
	/**
	 * retrieves a particular playlist
	 * 
	 * @param _channelId
	 * @return Playlist
	 */
	public Playlist loadPlaylist(long _channelId) {
		Log.i(TAG, "Queriying particular playlist (" + _channelId + ")...");
		Playlist playlist = null;
		synchronized(db) {
			Cursor cur = db.query(true, DATABASE_TABLE, null, KEY_CHANNEL
					+ "=" + _channelId, null, null, null, null, null);
			if (cur != null && cur.moveToFirst()) {
				playlist = createPlaylist(cur);
			}
		}
		return playlist;
	}
	
	/**
	 * retrieves all playlists
	 * 
	 * @param orderBy
	 * @return List
	 */
	public List<Playlist> loadPlaylists(String orderBy) {
		Log.i(TAG, "Queriying all playlists...");
		List<Playlist> playlists = new ArrayList<Playlist>();
		synchronized(db) {
			Cursor cur = db.query(DATABASE_TABLE, null, null, null, null, null, orderBy);
			if (cur != null && cur.moveToFirst()) {
				do {
					playlists.add(createPlaylist(cur));
				} while(cur.moveToNext());
			}
		}
		return playlists;
	}
	
	/**
	 * checks if the current playlist exists in the database
	 * 
	 * @return {@link #playlistExists(long)}
	 */
	public boolean playlistExists() {
		return playlistExists(currentPlaylist.getChannelId());
	}
	
	/**
	 * checks if a playlist exists in the database
	 * 
	 * @param _channelId
	 * @return true if playlist exists, false otherwise
	 */
	public boolean playlistExists(long _channelId) {
		Log.i(TAG, "Checking if particular playlist (" + _channelId + ") exists...");
		synchronized(db) {
			Cursor cur = db.query(true, DATABASE_TABLE, null, KEY_CHANNEL
					+ "=" + _channelId, null, null, null, null, null);
			return cur != null && cur.moveToFirst();
		}
	}
	
	/**
	 * creates a playlist from given cursor
	 * 
	 * @param cur
	 * @return Playlist
	 */
	public Playlist createPlaylist(Cursor cur) {
		// get column indices
		int idIndex = cur.getColumnIndex(KEY_ID);
		int channelIndex = cur.getColumnIndex(KEY_CHANNEL);
		int tracksIndex = cur.getColumnIndex(KEY_TRACKS);
		
		// get values of the row
		long id = cur.getLong(idIndex);
		long channel = cur.getLong(channelIndex);
		String tracks = cur.getString(tracksIndex);
		
		// create playlist and define attributes
		Playlist playlist = new Playlist(channel, true);
		playlist.setId(id);
		if (!tracks.isEmpty()) {
			String[] t = tracks.split(DELIMITER);
			for (String track : t) {
				playlist.getTrackIds().add(Long.parseLong(track.trim()));
			}
		}
		
		return playlist;
	}
	
	/**
	 * updates a playlist
	 * 
	 * @param playlist
	 * @return {@link SQLiteDatabase#update(String, ContentValues, String, String[])}
	 */
	public boolean updatePlaylist(Playlist playlist) {
		Log.i(TAG, "Updating playlist " + playlist.getId() + "...");
		ContentValues args = getContentValues(playlist);
		synchronized(db) {
			return db.update(DATABASE_TABLE, args, KEY_CHANNEL + "=" + playlist.getChannelId(), null) > 0;
		}
	}
	
	/**
	 * creates ContentValues with all attributes of the playlist
	 * 
	 * @param playlist
	 * @return ContentValues
	 */
	private ContentValues getContentValues(Playlist playlist) {
		ContentValues args = new ContentValues();
		args.put(KEY_CHANNEL, playlist.getChannelId());
		
		String tracks = "";
		for (long track : playlist.getTrackIds()) {
			if (!tracks.isEmpty())
				tracks += ", ";
			tracks += track;
		}
		args.put(KEY_TRACKS, tracks);
		
		return args;
	}
	
	/**
	 * loads the next track of the current playlist
	 * 
	 * @return track or null if no next track is available
	 */
	public Track getNextTrack() {
		if (currentPlaylist == null)
			return null;
		
		Long nextTrackId = currentPlaylist.next();
		if (nextTrackId == null)
			return null;
		
		Track nextTrack = mHandler.loadTrackWithHelpOfDatabaseId(nextTrackId, true);
		currentTrack = nextTrack;
		return nextTrack;
	}
	
	/**
	 * loads the previous track of the current playlist
	 * 
	 * @return track or null if no previous track is available
	 */
	public Track getPreviousTrack() {
		if (currentPlaylist == null)
			return null;
		
		Long prevTrackId = currentPlaylist.previous();
		if (prevTrackId == null)
			return null;
		
		Track prevTrack = mHandler.loadTrackWithHelpOfDatabaseId(prevTrackId, true);
		currentTrack = prevTrack;
		return prevTrack;
	}
	
	/**
	 * loads a particular track of the current playlist
	 * 
	 * @param position
	 * @return track or null of no track is available at this position
	 */
	public Track getTrackAtPosition(int position) {
		if (currentPlaylist == null)
			return null;
		
		Long trackId = currentPlaylist.get(position);
		if (trackId == null)
			return null;
		
		Track track = mHandler.loadTrackWithHelpOfDatabaseId(trackId, true);
		currentTrack = track;
		return track;
	}
	
	/**
	 * sets the current track
	 * 
	 * @param currentTrack
	 */
	public void setCurrentTrack(Track currentTrack) {
		this.currentTrack = currentTrack;
	}
	
	/**
	 * @return currentTrack
	 */
	public Track getCurrentTrack() {
		return currentTrack;
	}
	
	/**
	 * removes a track from all playlists
	 * 
	 * @param idDatabase
	 */
	public void removeTrackFromAllPlaylists(long idDatabase) {
		List<Playlist> playlists = loadPlaylists(null);
		for (Playlist p : playlists) {
			if (p.getTrackIds().remove(idDatabase))
				updatePlaylist(p);
		}
	}
	
	/**
	 * prefilters the given tracks
	 * if no tracks are given, it retrieves all tracks from the device
	 * 
	 * @param channel
	 * @param tracks
	 * @return List
	 */
	public List<Track> prefilter(Channel channel, ArrayList<Track> tracks,
			Weather currentWeatherConditions) {
		if (tracks == null) {
			Log.i(TAG, "Prefiltering all tracks on the device...");
			tracks = mHandler.loadTracks(null, null, true);
		} else {
			Log.i(TAG, "Prefiltering given tracks...");
		}
		
		// the current time of day
		TimeOfDay tod = TimeOfDay.get(Calendar.getInstance().get(Calendar.HOUR));
			
		List<Track> tracksAfterPrefiltering = new ArrayList<Track>();
		for (Track track : tracks) {
			if (track.getIdDatabase() > 0 && channel.matches(track, tod,
					currentWeatherConditions.getCurrentWeather())) {
				tracksAfterPrefiltering.add(track);
			}
		}
		
		return tracksAfterPrefiltering;
	}
	
	/**
	 * sends all track ids to our database and retrieves the computed tracklist
	 * 
	 * @param channel
	 * @param link
	 * @return Playlist
	 * @throws IOException 
	 * @throws ClientProtocolException 
	 * @throws ParserConfigurationException 
	 * @throws SAXException 
	 */
	public Playlist computePlaylist(Channel c, String link, Weather curweather) throws ClientProtocolException, IOException, ParserConfigurationException, SAXException {
		Log.i(TAG, "Query: " + link);
		Log.i(TAG, "Start parsing XML document to get a playlist...");
		LinkedList<Long> r0 = new LinkedList<Long>(); // all track-ids of tracks that fit to no contextual factor are stored here
		LinkedList<Long> r1 = new LinkedList<Long>(); // all track-ids of tracks that fit to one contextual factor are stored here
		LinkedList<Long> r2 = new LinkedList<Long>(); // all track-ids of tracks that fit to two contextual factors are stored here
		LinkedList<Long> r3 = new LinkedList<Long>(); // all track-ids of tracks that fit to three contextual factors are stored here
		LinkedList<Long> r4 = new LinkedList<Long>(); // all track-ids of tracks that fit to four contextual factors are stored here
		
		int activated = 1; // counts how many contextual factors are activated (mood is always observed)
		if(c.isConsideringSeason()){
			activated++;
		}
		if(c.isConsideringTimeOfDay()){
			activated++;
		}
		if(c.isConsideringWeather()){
			activated++;
		}
		
		TimeOfDay curtod = TimeOfDay.get(Calendar.getInstance().get(Calendar.HOUR)); // stores the current time of day
		Season curseason = Season.getCurrentSeason(Calendar.getInstance().get(Calendar.MONTH)); // stores the current season
		
		Document doc = XMLParser.getDocument(link);
		
		if(doc.getElementsByTagName("error").getLength() != 0){
			Log.i(TAG, "XML-File sends an error.");
		}else{
		
		Node getratings = doc.getElementsByTagName("ratings").item(0);
		Element e = (Element) getratings;
		
		Log.i(TAG, "Parsing ratings...");
		NodeList ratings = e.getElementsByTagName("rating");
		Log.i(TAG, "before for...");
		
		for(int i=0; i<ratings.getLength(); i++){
			Node n = ratings.item(i);
			Element cur = (Element) n;
			
			Log.i(TAG, "Parsing trackid...");
			Node curattr = cur.getElementsByTagName("id").item(0);
			Long curid = Long.parseLong(curattr.getTextContent().trim());
			
			int counter = 0; // counts how many contextual factors of the current track goes with the channel values
			
			Element h;
			
			try{
				Log.i(TAG, "Parsing moods...");
				Node m = cur.getElementsByTagName("moods").item(0);
				h = (Element) m;
				NodeList moods = h.getElementsByTagName("mood");
				
				for(int j=0; j<moods.getLength(); j++){
					curattr = moods.item(j);
					Mood mood = Mood.getById(Integer.parseInt(curattr.getTextContent().trim()));
					if(c.getMoods().contains(mood)){
						counter++;
						break;
					}
				}
			}catch(NullPointerException npe){
				Log.i(TAG, "No moods found...");
			}
			
			try{
				Log.i(TAG, "Parsing weather...");
				Node w = cur.getElementsByTagName("weathers").item(0);
				h = (Element) w;
				NodeList weathers = h.getElementsByTagName("weather");
				
				for(int j=0; j<weathers.getLength(); j++){
					curattr = weathers.item(j);
					Weather.Condition weather = Weather.Condition.valueOf(curattr.getTextContent().trim());
					if(weather == curweather.getCurrentWeather()){
						counter++;
						break;
					}
				}
			}catch(NullPointerException npe){
				Log.i(TAG, "No weather found...");
			}
			
			try{
				Log.i(TAG, "Parsing seasons...");
				Node s = cur.getElementsByTagName("seasons").item(0);
				h = (Element) s;
				NodeList seasons = h.getElementsByTagName("season");
				
				for(int j=0; j<seasons.getLength(); j++){
					curattr = seasons.item(j);
					System.out.println("\t season "+j+": '"+curattr.getTextContent()+"'\n");
					Season season = Season.valueOf(curattr.getTextContent().trim());
					if(curseason == season){
						counter++;
						break;
					}
				}
			}catch(NullPointerException npe){
				Log.i(TAG, "No Seasons found...");
			}
			
			try{
				Log.i(TAG, "Parsing timesOfDay...");
				Node t = cur.getElementsByTagName("timesOfDay").item(0);
				h = (Element) t;
				NodeList timesofday = h.getElementsByTagName("timeOfDay");
				
				for(int j=0; j<timesofday.getLength(); j++){
					curattr = timesofday.item(j);
					TimeOfDay time = TimeOfDay.valueOf(curattr.getTextContent().trim());
					if(curtod == time){
						counter++;
						break;
					}
				}
			}catch(NullPointerException npe){
				Log.i(TAG, "No timesOfDay found...");
			}
			
			// add the track to the adequate list
			
			if(counter==0){
				r0.add(curid);
			}else if(counter==1){
				r1.add(curid);
			}else if(counter==2){
				r2.add(curid);
			}else if(counter==3){
				r3.add(curid);
			}else if(counter==4){
				r4.add(curid);
			}
		}
		}
		
		Log.i(TAG, "Parsing finished!");
		
		Log.i(TAG, "Compute the ratingscores...");
		
		LinkedList<Long> tracks = new LinkedList<Long>(); // list with final song-id's
		
		List<ChannelTrackRating> ctratings = ctrHandler.getChannelTrackRatings(c);
		
		HashMap<Double,HashSet<Long>> hashmap = new HashMap<Double, HashSet<Long>>(); // hashmap: key = ratingscore, value = set of song-id's with this ratingscore
		
		long maxrating = 0;
		long minrating = 0;
		
		//compute the maximum and the minimum of implicit ratings
		for(ChannelTrackRating ctr : ctratings){
			if(ctr.getRatingValue()>maxrating){
				maxrating = ctr.getRatingValue();
			}
			if(ctr.getRatingValue()<minrating){
				minrating = ctr.getRatingValue();
			}
		}	
			
		for(ChannelTrackRating ctr : ctratings){
			long imprating = ctr.getRatingValue();
			double impratio = 0;
			double expratio;
			
			// normalize the implicit rating to get a value between -1 and +1
			if(imprating>0){
				impratio = imprating/maxrating;
			}else if(imprating<0){
				impratio = imprating/Math.abs(minrating);
			}
						
			double result;
			
			// if there is an explicit rating for the song with the current track-id:
			//		final score (result): 30 percent of normalized IMPLICITRATING + 70 percent of normalized EXPLICITRATING
			// else:
			// 		final score (result): 100 percent of normalized IMPLICITRATING
			if(r0.contains(ctr.getTrackId())){
				expratio = 2*((0/activated)-0.5);
				
				result = 0.3 * impratio + 0.7 * expratio;
			}else if(r1.contains(ctr.getTrackId())){
				expratio = 2*((1/activated)-0.5);
				
				result = 0.3 * impratio + 0.7 * expratio;
			}else if(r2.contains(ctr.getTrackId())){
				expratio = 2*((2/activated)-0.5);
				
				result = 0.3 * impratio + 0.7 * expratio;
			}else if(r3.contains(ctr.getTrackId())){
				expratio = 2*((3/activated)-0.5);
				
				result = 0.3 * impratio + 0.7 * expratio;
			}else if(r4.contains(ctr.getTrackId())){
				expratio = 2*((4/activated)-0.5);
				
				result = 0.3 * impratio + 0.7 * expratio;
			}else{
				result = impratio;
			}
			
			
			// add the result to the hashmap
			HashSet<Long> acttracks = hashmap.get(result);
			if(acttracks != null){
				acttracks.add(ctr.getTrackId());
				hashmap.put(result, acttracks);
			}else{
				HashSet<Long> help = new HashSet<Long>();
				help.add(ctr.getTrackId());
				hashmap.put(result, help);
			}			
		}
		
		Log.i(TAG, "Ratingscores successfully computed!");
		
		ArrayList<Double> sortedratings = new ArrayList<Double>();
		
		for(Double d : hashmap.keySet()){
			sortedratings.add(d);
		}
		
		java.util.Collections.sort(sortedratings);
		
		for(int i=0; i<40; i++){
			int last = sortedratings.size()-1;
			
			boolean added = false;
			
			if(last<0){
				break;
			}
			
			double currating = sortedratings.get(last);
			sortedratings.remove(last);
			
			HashSet<Long> curtracks = hashmap.get(currating);
			
			for(Long curtrack : curtracks){
				if(i<40){
					tracks.add(curtrack);
					added = true;
					Log.i(TAG, "Track " + curtrack + " with combined rating value " + currating + " added!");
					Log.i(TAG, "counter 1st: "+i);
					i++;
				}
			}
			
			if(added){
				i--;
			}
			
			Log.i(TAG, "counter 2nd: "+i);
		}
		
		Playlist pl = new Playlist(c.getId(), true);
		
		Log.i(TAG, "Creating playlist with size " + tracks.size() + "...");
		
		for(Long i:tracks){
			Track t = mHandler.loadTrackWithHelpOfDatabaseId(i, true);
			if(t == null){
				continue;
			}
			pl.appendTrack(t.getIdDatabase());
		}
		

		Log.i(TAG, "Playlist created!");
		
		return pl;
	}
}
